import fs from 'fs';
import path from 'path';
import EventEmitter from 'events';
import request from './request';
import lzma from '../lzma';
import { isString, existsPromisify, loger, fileMd5, mkdirSync, unlinkSync } from '../util';

export const events = new EventEmitter();

const numberTryParse = (v) => {
  return isNaN(Number(v)) ? v : Number(v);
};
const semverCompare = (ver1, ver2) => {
  ver1 = ver1 || '';
  ver2 = ver2 || '';
  const arr1 = ver1.split('.').map(numberTryParse);
  const arr2 = ver2.split('.').map(numberTryParse);
  if (arr1[0] != arr2[0]) {
    return arr1[0] > arr2[0];
  }
  if (arr1[1] != arr2[1]) {
    return arr1[1] > arr2[1];
  }
  if (arr1[2] != arr2[2]) {
    return arr1[2] > arr2[2];
  }
  return false;
};
/**
 * 检查版本更新
 */
export const checkVersion = ({ name, oldVersion = '', isSemver, urlDownload }) => {
  const url = `${urlDownload}${name}/release.json`;
  return request(url)
    .then(({ data }) => {
      if (data) {
        if (isString(data)) data = JSON.parse(data);
        if (data.version) {
          return {
            oldVersion: oldVersion,
            ...data,
            hasUpdate: isSemver ? semverCompare(data.version, oldVersion) : data.version > oldVersion,
          };
        }
      }
    })
    .catch((err) => {
      loger.error('checkVersion', url, err);
      return {};
    });
};

/**
 * 检测文件是否一致
 * @param {*} args
 */
export const checkSame = ({ filePath, fileMd5: md5Check }) => {
  return existsPromisify(filePath).then((exists) => {
    if (!exists) {
      return false;
    }

    return (
      md5Check &&
      fileMd5(filePath).then((md5) => {
        loger.debug('checkSame', md5Check, md5, filePath);
        return md5 === md5Check;
      })
    );
  });
};

/**
 * 是否过期
 * @param {*} date
 * @param {Number} num 分钟
 */
export const isExpire = (date, num) => {
  if (!date) {
    return true;
  }
  if (isString(date)) {
    date = new Date(date);
  }
  date.setMinutes(date.getMinutes() + num);
  return date < new Date();
};

/**
 * lzma解压
 * @param {*} data
 */
const decompressPromise = (data) =>
  new Promise((resolve, reject) => {
    lzma.decompress(data, (result, error) => {
      if (error) {
        reject(error);
      } else {
        resolve(Buffer.from(result));
      }
    });
  });

/**
 * 关闭句柄
 * @param {*} fd
 */
const fdEnd = (fd) =>
  new Promise((resolve) => {
    return fd.end(() => resolve());
  });

/**
 * 下载配置文件读取
 * @param {*} path
 * @param {*} def
 */
const fileCfgRead = (path) => {
  try {
    const cfg = fs.readFileSync(path, { encoding: 'utf8' });
    return JSON.parse(cfg);
  } catch (error) {
    return {};
  }
};

const fileCfgSave = (path, cfg) => {
  try {
    fs.writeFileSync(path, JSON.stringify(cfg), { encoding: 'utf8' });
  } catch (error) {
    //
  }
};

const fetchProgress = (data, { start, end, position, fileSave, timeout, fileUrl, index, onProgress, taskId }) => {
  let resTimeout;
  let abort = false;
  let accumBytes = 0;
  let canceled = false;
  const size = end - start + 1;
  const fd = fs.createWriteStream(fileSave, { start: position, encoding: null, flags: 'r+' });
  events.once(taskId, () => {
    canceled = true;
  });
  return new Promise((resolve, reject) => {
    if (timeout) {
      resTimeout = setTimeout(function () {
        abort = true;
        reject(new Error(`Response timeout while trying to fetch ${fileUrl} (over ${timeout}ms)`, 'body-timeout'));
      }, timeout);
    }
    data.on('error', function (err) {
      if (err.name === 'AbortError') {
        // if the request was aborted, reject with this Error
        abort = true;
        reject(err);
      } else {
        // other errors, such as incorrect content-encoding
        reject(new Error(`Invalid response body while trying to fetch ${fileUrl}: ${err.message}`, 'system', err));
      }
    });
    data.on('data', function (chunk) {
      if (abort || chunk === null) {
        return;
      }
      if (canceled) {
        reject(new Error('canceled'));
        return;
      }
      if (size && accumBytes + chunk.length > size) {
        abort = true;
        reject(new Error(`content size at ${fileUrl} over limit: ${size}`, 'max-size'));
        return;
      }
      accumBytes += chunk.length;
      fd.write(chunk, () => {
        onProgress(index, [start, end, position + accumBytes]);
      });
    });

    data.on('end', function () {
      if (abort) {
        return;
      }

      clearTimeout(resTimeout);
      try {
        resolve();
      } catch (err) {
        // handle streams that have accumulated too much data (issue #414)
        reject(new Error(`Could not create Buffer from response body for ${fileUrl}: ${err.message}`, 'system', err));
      }
    });
  })
    .catch((error) => {
      return error;
    })
    .then((error) => {
      return fdEnd(fd).then(() => {
        onProgress(index, [start, end, position + accumBytes]);
        if (error) {
          throw error;
        }
      });
    });
};

const CONTENT_TYPE = 'Content-Type';
const OCTET_STREAM = 'application/octet-stream';

/**
 * 下载文件
 * @param {*} args
 */
export const download = (args) => {
  return checkSame(args).then((isSame) => {
    const { fileUrl, filePath, fileMd5, isLzma = 1, timeout, onEvent = () => {}, taskId } = args;
    onEvent('shouldDownload', { ...args, isSame });
    if (isSame) {
      return { downStatus: 2 };
    }
    const fileSave = `${filePath}.down`;
    const fileCfg = `${fileSave}.cfg`;
    let threads = args.threads || 1;
    return request(fileUrl, { method: 'head', headers: { [CONTENT_TYPE]: OCTET_STREAM } })
      .then((response) => {
        const { err, res: { headers = {} } = {} } = response;
        if (err) throw err;
        const {
          'content-length': [contentLengthStr],
          'accept-ranges': [acceptRanges],
          'last-modified': [lastModified],
          etag: [etag],
        } = headers.raw();
        // 若无内容长度或支持的range类型 则用单线程下载
        if (!contentLengthStr || !acceptRanges) {
          threads = 1;
        }
        const contentLength = parseInt(contentLengthStr, 10);
        mkdirSync(path.dirname(fileSave));
        // 断点下载配置

        let cfg = fileCfgRead(fileCfg);
        if (
          cfg.threads !== threads ||
          cfg.fileUrl !== fileUrl ||
          cfg.fileMd5 !== fileMd5 ||
          cfg.isLzma !== isLzma ||
          cfg.etag !== etag ||
          cfg.lastModified !== lastModified ||
          cfg.contentLength !== contentLength ||
          !fs.existsSync(fileSave)
        ) {
          //创建空文件
          fs.writeFileSync(fileSave, '');
          cfg = { threads, fileUrl, fileMd5, isLzma, etag, lastModified, contentLength };
          fileCfgSave(fileCfg, cfg);
        }
        const segmentLength = Math.floor(contentLength / threads);
        const threadsPs = [];
        let lastProgress = 0;
        let lastProgressTimer;
        const onProgress = (index, data) => {
          cfg[index] = data;
          const now = new Date().getTime();
          if (lastProgressTimer) {
            clearTimeout(lastProgressTimer);
          }
          if (now > lastProgress + 500) {
            fileCfgSave(fileCfg, cfg);
            onEvent('progress', cfg);
            lastProgress = now;
          } else {
            lastProgressTimer = setTimeout(() => {
              fileCfgSave(fileCfg, cfg);
              onEvent('progress', cfg);
              lastProgress = new Date().getTime();
            }, 1000);
          }
        };
        for (let index = 1; index <= threads; index++) {
          const threadCfg = cfg[index] || [];
          let [start, end, position = 0] = threadCfg;
          start = start || segmentLength * (index - 1);
          end = end || (index === threads ? contentLength - 1 : segmentLength * index - 1);
          position = position || start;
          // position记录的为下一次开始的位置
          const ps =
            position >= end
              ? Promise.resolve()
              : request(fileUrl, {
                  rawData: 1,
                  headers: { Range: `${acceptRanges}=${position}-${end}`, [CONTENT_TYPE]: OCTET_STREAM },
                }).then(({ err, data }) => {
                  if (err) throw err;
                  return fetchProgress(data, {
                    start,
                    end,
                    position,
                    timeout,
                    fileUrl,
                    fileSave,
                    onProgress,
                    index,
                    taskId,
                  });
                });
          threadsPs.push(ps);
        }
        return Promise.all(threadsPs).then(() => {
          if (lastProgressTimer) {
            clearTimeout(lastProgressTimer);
          }
        });
      })
      .then(() => {
        if (isLzma) {
          if (!fs.existsSync(fileSave)) {
            onEvent('error', new Error(`文件不存在。\n${fileSave}.`));
            return;
          }
          const data = fs.readFileSync(fileSave);
          return decompressPromise(data).then((result) => {
            unlinkSync(fileSave);
            mkdirSync(path.dirname(fileSave));
            fs.writeFileSync(fileSave, Buffer.from(result));
          });
        }
      })
      .then(() =>
        checkSame({ filePath: fileSave, fileMd5 }).then((isSame) => {
          const res = { ...args, fileSave, downStatus: isSame ? 1 : 0 };
          try {
            unlinkSync(fileCfg);
          } catch (error) {
            //
          }
          if (isSame) {
            onEvent('downloaded', res);
            return res;
          }
          throw res;
        }),
      )
      .catch((error) => {
        if (error.message !== 'canceled') {
          onEvent('error', error);
          throw error;
        }
      })
      .finally(() => {
        events.removeAllListeners(taskId);
      });
  });
};
